﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Collections;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.Xml.Linq;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Collections.Specialized;

using Microsoft.Build.Utilities;

using King.Text;
using King.Extensions;
using King.Xml;
using King.Reflection;
using King.PostBuild;

namespace King.Automation {

    public sealed class XCommand {
        internal sealed class Tag { }
        public static readonly XName Console = "console";
    }

    public abstract class CommandException : Exception {
        internal CommandException(string message) : base(message) { }
    }
    public class CommandBindException : CommandException {
        internal CommandBindException(string message) : base(message) { }
    }
    public class CommandLoadException : CommandException {
        internal CommandLoadException(string message) : base(message) { }
    }
    public class CommandParseException : CommandException {
        internal CommandParseException(string message) : base(message) { }
    }

    public sealed class CommandAssembly {

        public static CommandAssembly GetExecutingAssembly() {
            return GetAssembly(Assembly.GetCallingAssembly());
        }
        public static CommandAssembly GetAssembly(Assembly assembly) {
            CommandAssembly cached;

            if (s_assemblyToCommandAssembly == null)
                s_assemblyToCommandAssembly = new Dictionary<Assembly, CommandAssembly>();

            if (!s_assemblyToCommandAssembly.TryGetValue(assembly, out cached))
                s_assemblyToCommandAssembly[assembly] = cached = new CommandAssembly(assembly);

            return cached;
        }

        private static void LoadEverything() {
        }

        private static Dictionary<Assembly, CommandAssembly> s_assemblyToCommandAssembly;

        private Assembly m_assembly;
        private IEnumerable<CommandInfo> m_commands;
        private Dictionary<Type, CommandInfo> m_typeToCommandInfo;
        private Dictionary<string, CommandInfo> m_nameToCommandInfo;
        private string m_company, m_description, m_version, m_copyright;

        internal CommandAssembly(Assembly assembly) {
            Contract.Requires(assembly.IsDefined<AssemblyCompanyAttribute>());
            Contract.Requires(assembly.IsDefined<AssemblyDescriptionAttribute>());
            Contract.Requires(assembly.IsDefined<AssemblyVersionAttribute>());
            Contract.Requires(assembly.IsDefined<AssemblyCopyrightAttribute>());

            m_assembly = assembly;
            m_typeToCommandInfo = new Dictionary<Type, CommandInfo>();

            m_company = m_assembly.GetAttribute<AssemblyCompanyAttribute>().Company;
            if (string.IsNullOrEmpty(m_company))
                throw LoadException("must attribute a company by attributing its assembly with AssemblyCompanyAttribute.");

            m_description = m_assembly.GetAttribute<AssemblyDescriptionAttribute>().Description;
            if (string.IsNullOrEmpty(m_description))
                throw LoadException("must provide a description by attributing its assembly with AssemblyDescriptionAttribute.");

            m_version = m_assembly.GetAttribute<AssemblyFileVersionAttribute>().Version;
            if (string.IsNullOrEmpty(m_version))
                throw LoadException("must specify a company by attributing its assembly with AssemblyFileVersionAttribute.");

            m_copyright = m_assembly.GetAttribute<AssemblyCopyrightAttribute>().Copyright;
            if (string.IsNullOrEmpty(m_copyright))
                throw LoadException("must supply a copyright by attributing its assembly with AssemblyCopyrightAttribute.");

        }
        private T Exception<T>(string message) where T : CommandException {
            return (T)typeof(T).Activate(string.Format("Assembly {0} {1}", Name, message));
        }
        private CommandLoadException LoadException(string message) {
            return Exception<CommandLoadException>(message);
        }

        public string Company {
            get { return m_company; }
        }
        public string Description {
            get { return m_description; }
        }
        public string Version {
            get { return m_version; }
        }
        public string Copyright {
            get { return m_copyright; }
        }
        public string Name {
            get { return m_assembly.GetName().Name; }
        }

        public Assembly Assembly {
            get { return m_assembly; }
        }
        public CommandInfo GetCommand(Type type) {
            Contract.Requires(type != null);
            Contract.Requires(type.IsDefined<CommandAttribute>());
            Contract.Requires(type.IsSubclassOf(typeof(Command)));

            CommandInfo cached;
            if (!m_typeToCommandInfo.TryGetValue(type, out cached))
                m_typeToCommandInfo[type] = cached = new CommandInfo(this, type);
            return cached;
        }
        public CommandInfo GetCommand(string name) {
            Contract.Requires(name != null);

            if (m_nameToCommandInfo == null)
                m_nameToCommandInfo = Commands().ToCaseInsensitiveDictionary(o => o.Name);

            return m_nameToCommandInfo.GetValueOrDefault(name);
        }
        public IEnumerable<CommandInfo> Commands() {
            if (m_commands == null) {
                var types =
                    from o in m_assembly.GetTypes()
                    where o.IsDefined<CommandAttribute>() && o.IsSubclassOf(typeof(Command))
                    select o;

                foreach (var type in types)
                    GetCommand(type);

                var commands = m_typeToCommandInfo.Values.OrderBy(o => o.Name).ToList();
                var duplicateCommands = commands.GroupBy(o => o.Name.ToLower()).Where(o => o.Count() > 1);
                if (duplicateCommands.Any())
                    throw LoadException("has commands with conflicting names declared in " +
                        new TextJoin(", ") { duplicateCommands.First().Select(o => o.Type) }.ReadToEnd());

                m_commands = commands;
            }

            return m_commands;
        }
    }
    public sealed class CommandInfo {

        public static CommandInfo GetCommand(string name) {
            Contract.Requires(name != null);

            return CommandAssembly.GetAssembly(
                System.Reflection.Assembly.GetCallingAssembly()).GetCommand(name);
        }
        public static CommandInfo GetCommand(Type type) {
            Contract.Requires(type != null);
            Contract.Requires(type.IsDefined<CommandAttribute>());
            Contract.Requires(type.IsSubclassOf(typeof(Command)));

            return CommandAssembly.GetAssembly(type.Assembly).GetCommand(type);
        }

        private Type m_type;
        private CommandAssembly m_commandAssembly;
        private string m_name;
        private int? m_order;
        private IEnumerable<CommandMethodInfo> m_methods;
        private CommandMethodInfo m_constructor;

        internal CommandInfo(CommandAssembly commandAssembly, Type type) {
            Contract.Assert(commandAssembly != null);
            Contract.Requires(Name.IsIdentifier());
            Contract.Ensures(type.IsSubclassOf(typeof(Command)));

            m_type = type;
            m_commandAssembly = commandAssembly;

            if (!type.IsSubclassOf(typeof(Command)))
                throw LoadException("cannot be loaded as a command because is not a subclass of Command.");

            var commandAttribute = type.GetAttribute<CommandAttribute>();
            if (commandAttribute != null)
                m_name = commandAttribute.Name ?? type.Name;

            if (!m_name.IsIdentifier())
                throw LoadException(
                    "cannot be loaded as a command becuase its name '" + m_name + "' contains " +
                    "characters not allowed in an identifier.");

            var constructors =
                from o in type.Get<ConstructorInfo>()
                where o.IsDefined<CommandMethodInfo>()
                select o;
            if (constructors.MoreThanOne())
                throw LoadException(
                    "cannot be loaded because it contains more than one constructor annotated with CommandMethodInfo.");

            if (constructors.None())
                constructors = type.Get<ConstructorInfo>(types: Type.EmptyTypes);
            if (constructors.None())
                throw LoadException(
                    "cannot be loaded because it has no parameterless constructor and no constructor annotated with CommandMethodInfo.");

            m_constructor = new CommandMethodInfo(this, constructors.Single());
        }
        private T Exception<T>(string message) where T : CommandException {
            return (T)typeof(T).Activate(string.Format("Command {0} {1}", m_type.AssemblyQualifiedName, message));
        }
        private CommandLoadException LoadException(string message) {
            return Exception<CommandLoadException>(message);
        }

        public Type Type {
            get { return m_type; }
        }
        public CommandAssembly Assembly {
            get { return m_commandAssembly; }
        }
        public IEnumerable<CommandMethodInfo> Methods() {
            if (m_methods == null) {
                m_methods =
                   (from o in m_type.Get<MethodInfo>()
                    where o.IsDefined<CommandMethodAttribute>()
                    select new CommandMethodInfo(this, o))
                    .ToArray();

                var defaultMethods = m_methods.Where(o => !o.IsVerb).GroupBy(o => o.MethodBase.Name);
                if (defaultMethods.MoreThanOne())
                    throw LoadException(
                        "must use the same name all default command methods. Currently more that one name is uesed: " +
                        defaultMethods.StringJoin(", ", o => o.Key));
            }
            return m_methods;
        }
        public IEnumerable<CommandMethodInfo> Methods(string name) {
            return Methods().Where(o => name.CaseInsensitveEquals(o.Name));
        }
        public IEnumerable<CommandMethodInfo> Methods(IEnumerable<string> commandLine) {
            var methods = Methods();

            // match verbs to arguments
            var verbs =
               (from o in methods
                where o.IsVerb
                join a in commandLine.Skip(1) on "/" + o.Name.ToLower() equals a.ToLower()
                select o).ToArray();

            // one (or more verbs) was specified
            if (verbs.Any())
                methods = verbs;

            // no verbs were specified
            else
                methods = methods.Where(o => !o.IsVerb);

            return methods;
        }
        public CommandMethodInfo Constructor {
            get { return m_constructor; }
        }

        public string Name {
            get { return m_name; }
        }
        public string Documentation {
            get { return m_type.GetDocumentation(true); }
        }
        public int? Order {
            get { return m_order; }
        }

        public override string ToString() {
            return Name;
        }
    }
    public sealed class CommandMethodInfo {

        private CommandInfo m_commandInfo;
        private MethodBase m_methodBase;
        private IEnumerable<CommandParameterInfo> m_parameters;
        private bool m_isVerb;

        public CommandMethodInfo(CommandInfo commandInfo, MethodBase methodBase) {
            m_commandInfo = commandInfo;
            m_methodBase = methodBase;
            m_isVerb = m_methodBase.IsDefined<CommandVerbAttribute>();
        }
        private T Exception<T>(string message) where T : CommandException {
            return (T)typeof(T).Activate(string.Format("Command {0} {1} {2}", 
                m_commandInfo.Type.AssemblyQualifiedName, 
                IsVerb ? "/" + Name.ToLowerFirst() : null,
                message));
        }
        private CommandLoadException LoadException(string message) {
            return Exception<CommandLoadException>(message);
        }
        private CommandBindException BindException(string message) {
            return Exception<CommandBindException>(message);
        }
        private CommandParseException ParseException(string message) {
            return Exception<CommandParseException>(message);
        }

        public CommandInfo Command {
            get { return m_commandInfo; }
        }
        public MethodBase MethodBase {
            get { return m_methodBase; }
        }
        public bool IsVerb {
            get { return m_isVerb; }
        }
        public bool IsConstructor {
            get { return m_methodBase is ConstructorInfo; }
        }
        public bool IsStatic {
            get {
                if (IsConstructor)
                    return false;
                return ((MethodInfo)m_methodBase).IsStatic;
            }
        }
        public string Name {
            get { return m_methodBase.Name; }
        }
        public IEnumerable<CommandParameterInfo> Parameters() {
            if (m_parameters == null)
                m_parameters = m_methodBase.GetParameters().Select(o => new CommandParameterInfo(this, o)).ToArray();
            return m_parameters;
        }
        public CommandParameterInfo GetParameter(string name) {
            return Parameters().SingleOrDefault(o => o.Name.ToLower() == name.ToLower());
        }
        public IEnumerable<CommandParameterInfo> Targets() {
            var ctorTargets = Enumerable.Empty<CommandParameterInfo>();
            if (!IsConstructor && !IsStatic)
                ctorTargets = Command.Constructor.Targets();

            return ctorTargets.Concat(Parameters().Where(o => o.IsTarget));
        }
        public IEnumerable<CommandParameterInfo> Switches() {
            var ctorSwitches = Enumerable.Empty<CommandParameterInfo>();
            if (!IsConstructor && !IsStatic)
                ctorSwitches = Command.Constructor.Switches();

            return ctorSwitches.Concat(Parameters().Where(o => o.IsSwitch));
        }

        public Exception TryBind(IEnumerable<string> arguments, out Dictionary<CommandParameterInfo, string> result) {

            result = new Dictionary<CommandParameterInfo, string>();

            // track switches not yet seen and disallow specifying the same switch twice
            var switches = Switches().ToCaseInsensitiveHashSet(o => o.Name);

            // track the current target by moving through an enumeration of targets sorted by order
            var targets = Targets().GetEnumerator();

            // switches have the form /name:value
            var switchRegex = new Regex(@"^/(?<name>\w*)(:(?<value>.*))?$", RegexOptions.Singleline);

            // deserialize each argument into a switch or target
            foreach (var argument in arguments) {
                var switchMatch = switchRegex.Match(argument);

                // process switch
                if (switchMatch.Success) {
                    var switchName = switchMatch.Get("name");
                    var stringValue = switchMatch.Get("value");

                    // bind switch by name
                    var parameter = GetParameter(switchName);
                    if (parameter == null)
                        return ParseException("has no switch '" + switchName + "'.");

                    // require unique switch
                    if (!switches.Contains(parameter.Name))
                        return ParseException("cannot parse switch '" + switchName + "' more than once.");
                    switches.Remove(parameter.Name);

                    // only boolean switches can have a missing value
                    var type = parameter.Type.RuntimeType;
                    if (string.IsNullOrEmpty(stringValue) && !new[] {
                        typeof(bool), typeof(bool?) }.Contains(type))
                        return ParseException("must provide a value for non-boolean switch '" + switchName + "'.");

                    result[parameter] = stringValue;

                    // process target
                } else {

                    // ensure target exists
                    if (!targets.MoveNext())
                        return ParseException("cannot parse extra target '" + argument + "'.");

                    result[targets.Current] = argument;
                }
            }

            return null;
        }
        public object Execute(Dictionary<CommandParameterInfo, object> arguments) {
            Contract.Requires(!IsConstructor);
            Contract.Requires(arguments != null);

            var ctorArguments = arguments.Where(o => o.Key.Method.IsConstructor);
            var command = Command.Type.Invoke(".ctor",
                arguments: ctorArguments.Select(o => o.Value),
                parameterNames: ctorArguments.Select(o => o.Key.ParameterInfo.Name));

            var methodArguments = arguments.Where(o => !o.Key.Method.IsConstructor);
            var result = Command.Type.Invoke(m_methodBase.Name,
                arguments: methodArguments.Select(o => o.Value),
                parameterNames: methodArguments.Select(o => o.Key.ParameterInfo.Name));

            return result;
        }
    }
    public sealed class CommandParameterInfo {

        private ParameterInfo m_parameterInfo;
        private CommandMethodInfo m_method;
        private CommandType m_commandType;
        private CommandPathInfo? m_path;
        private bool m_isSwitch;

        internal CommandParameterInfo(CommandMethodInfo method, ParameterInfo parameterInfo) {
            Contract.Ensures(Type.ElementTypeOrSelf != typeof(string) || !IsPath);
            Contract.Ensures(Type.ElementTypeOrSelf != typeof(string) || string.Empty != (string)DefaultValue);
            Contract.Ensures(Type.ElementTypeOrSelf != typeof(string) || !Type.IsEnumeration || DefaultValue == null ||
                !((IEnumerable)DefaultValue).Cast<object>().Any(o => o == null));
            Contract.Ensures(Name.IsIdentifier());
            Contract.Ensures(Order > 0);
            Contract.Ensures(IsRequired || DefaultValue == null);
            Contract.Ensures(IsOptional || DefaultValue != null || !Type.IsNullable);

            m_parameterInfo = parameterInfo;
            m_method = method;

            m_isSwitch = parameterInfo.IsOptional;

            var attribute = parameterInfo.GetAttribute<CommandParameterAttribute>();
            if (attribute != null) {
                if (attribute is CommandTargetAttribute)
                    m_isSwitch = false;
                else if (attribute is CommandSwitchAttribute)
                    m_isSwitch = true;

                m_path = attribute.Path;
            }

            m_commandType = CommandType.GetCommandType(m_parameterInfo.ParameterType);

            // require parameters that are directories or files be strings
            if (IsPath && Type.ElementTypeOrSelf != typeof(string))
                throw LoadException("is a path but is not a string or IEnumerable<string>.");

            // require that required parameters or optional parameter with default values not be nullable
            if ((IsRequired || DefaultValue != null) && m_commandType.IsNullable)
                throw LoadException("is nullable yet is required or has a default value.");
        }
        private T Exception<T>(string message) where T : CommandException {
            return (T)typeof(T).Activate(string.Format("Parmater '{0}' on method '{1}' on command '{2}' {2}", 
                Name, m_method.Name, Method.Name, message));
        }
        private CommandLoadException LoadException(string message) {
            return Exception<CommandLoadException>(message);
        }
        private CommandBindException BindException(string message) {
            return Exception<CommandBindException>(message);
        }

        private object GetDefaultValue() {
            object value = null;

            // assign a constant default value supplied in the CommandParameterAttribute
            if (DefaultValue != null)
                value = DefaultValue;

            // verify required arguments are present
            if (value == null && IsRequired)
                throw BindException("is missing.");

            // normalize enumerations to be empty instead of null
            if (Type.IsEnumeration && value == null)
                value = Array.CreateInstance(Type.ElementTypeOrSelf, 0);

            return value;
        }
        private object BindArgumentElement(object argument) {

            //// ensure that the empty string is replaced by null
            //if (argument is string && ((string)argument) == string.Empty)
            //    argument = null;

            //if (argument == null)
            //    return null;

            //// ensure the string(s) all end with a backslash
            //if (IsDirectory)
            //    argument = ((string)argument).NormalizeDirectory();

            //// make path absolute by using the current as the base path
            //if (IsRelativeToCurrentDirectory)
            //    argument = new Uri(new Uri(Environment.CurrentDirectory.NormalizeDirectory()), (string)argument).LocalPath;

            //// ensure directory exists
            //if (IsExistingDirectory && !Directory.Exists((string)argument))
            //    throw BindException("cannot be bound to " + argument + " because the directory does not exist.");

            //// ensure file or directory exists
            //if (IsExistingDirectoryOrFile && !Directory.Exists((string)argument) && !File.Exists((string)argument))
            //    throw BindException("cannot be bound to " + argument + " because the directory or file do not exist.");

            //// ensure file exists
            //if (IsExistingFile && !File.Exists((string)argument))
            //    throw BindException("cannot be bound to " + argument + " because the file does not exist.");

            //// enum's that are not flags must be one of the named value
            //if (Type.ElementTypeOrSelf.RuntimeType.IsEnum &&
            //    !Enum.GetValues(Type.ElementTypeOrSelf).Cast<object>().Any(o => o.Equals(argument)))
            //    throw BindException("cannot be bound to " + argument + " because that enum value has no name.");

            return argument;
        }

        public ParameterInfo ParameterInfo {
            get { return m_parameterInfo; }
        }
        public CommandInfo Command {
            get { return Method.Command; }
        }
        public CommandMethodInfo Method {
            get { return m_method; }
        }
        public CommandType Type {
            get { return m_commandType; }
        }
        public int Order {
            get { return m_parameterInfo.Position; }
        }
        public string Name {
            get { return m_parameterInfo.Name; }
        }
        public string Documentation {
            get { return m_parameterInfo.GetDocumentation(true); }
        }

        public bool IsPath {
            get { return m_path != null; }
        }
        public bool IsDirectory {
            get { return IsPath && (m_path | CommandPathInfo.TypeMask) == CommandPathInfo.IsDirectory; }
        }
        public bool IsFile {
            get { return IsPath && (m_path | CommandPathInfo.TypeMask) == CommandPathInfo.IsFile; }
        }

        public bool IsExistingPath {
            get { return IsPath && (m_path & CommandPathInfo.Exists) != 0; }
        }
        public bool IsRelativeToCurrentDirectory {
            get { return IsPath && (m_path & CommandPathInfo.RelativeToCurrentDirectory) != 0; }
        }

        public bool IsRequired {
            get { return !IsOptional; }
        }
        public bool IsOptional {
            get { return m_parameterInfo.IsOptional; }
        }
        public bool IsTarget {
            get { return !IsSwitch; }
        }
        public bool IsSwitch {
            get { return m_isSwitch; }
        }
        public object DefaultValue {
            get { return m_parameterInfo.DefaultValue; }
        }

        public object ToObject(object argument) {
            Contract.Requires(m_parameterInfo.ParameterType.CanAssignValue(argument));

            //// if argument is not an IEnumerable then initialize it else initialize each element
            //if (Type.IsEnumeration) {

            //    // verify no element is null
            //    if (((IEnumerable)argument).Cast<object>().Any(o => o == null))
            //        throw BindException("cannot be bound to an enumeration with a null or empty string element.");

            //} else {
            //    argument = BindArgumentElement(argument);
            //}

            //// basic type check
            //if (!m_parameterInfo.ParameterType.CanAssignValue(argument))
            //    throw BindException("cannot be bound to a value that cannot be stored in the parameter type.");

            return argument;
        }

        public override string ToString() {
            return (IsSwitch ? "/" : string.Empty) + Name;
        }
    }

    [Flags]
    public enum CommandPathInfo {

        /// <summary>
        /// The path is a directory file. Only has effect if Exists is also passed.
        /// </summary>
        IsFileOrDirectory = 0,
        
        /// <summary>
        /// The path is a file. Only has effect if Exists is also passed.
        /// </summary>
        IsFile = 1,

        /// <summary>
        /// Ensure the path, if supplied, ends with a backslash.
        /// </summary>
        IsDirectory = 2,

        TypeMask = IsFileOrDirectory | IsFile | IsDirectory,

        /// <summary>
        /// The file or directory must exist.
        /// </summary>
        Exists = 1 << 3,

        /// <summary>
        /// If a relative path is supplied then convert it to an absolute path using the current directory.
        /// </summary>
        RelativeToCurrentDirectory = 1 << 4,
    }

    public sealed class CommandType {

        public static implicit operator Type(CommandType type) {
            return type.RuntimeType;
        }
        public static CommandType GetCommandType(Type type) {
            if (s_dictionary == null)
                s_dictionary = new Dictionary<Type, CommandType>();

            CommandType commandType;
            if (!s_dictionary.TryGetValue(type, out commandType))
                s_dictionary[type] = commandType = new CommandType(type);

            return commandType;
        }

        private static Dictionary<Type, CommandType> s_dictionary;

        private Type m_type;
        private CommandType m_elementType;
        private bool m_isEnumeration;
        private bool m_isNullable;

        internal CommandType(Type type) {
            m_type = type;

            // initialize the element type if the type is an enumeration
            if (m_type.IsInstantiationOf(typeof(IEnumerable<>))) {
                m_isEnumeration = true;
                m_elementType = GetCommandType(m_type.GetGenericArguments().Single());

                if (m_elementType.IsNullable)
                    throw LoadException("cannot be loaded because the element type is nullable.");

                if (m_elementType.IsEnumeration)
                    throw LoadException("cannot be loaded because the element type is IEnumerable.");

                // strip the element type of its nullability
            } else if (m_type.IsNullable()) {
                m_isNullable = true;
                m_elementType = GetCommandType(m_type.MakeNotNullable());

            } else {

                // require the element type be in the set { string, enum, primatives + decimal }
                if (m_type != typeof(string) && !m_type.IsEnum && !m_type.IsPrimitive && m_type != typeof(decimal))
                    throw LoadException("cannot be loaded because it is not a string, enum, primitive or decimal.");

                // require enums not be flags
                if (m_type.IsEnum && m_type.IsDefined<FlagsAttribute>())
                    throw LoadException("cannot be loaded because it is attributed with the FlagsAttribute.");
            }
        }
        private T Exception<T>(string message) where T : CommandException {
            return (T)typeof(T).Activate(string.Format("CommandType of type {0} {1}", this, message));
        }
        private CommandLoadException LoadException(string message) {
            return Exception<CommandLoadException>(message);
        }
        private CommandParseException ParseException(string message) {
            return Exception<CommandParseException>(message);
        }

        public Type RuntimeType {
            get { return m_type; }
        }
        public CommandType ElementType {
            get { return m_elementType; }
        }
        public CommandType ElementTypeOrSelf {
            get { return ElementType ?? this; }
        }
        public bool IsEnumeration {
            get { return m_isEnumeration; }
        }
        public bool IsNullable {
            get { return m_isNullable; }
        }
        public bool IsString {
            get { return ElementTypeOrSelf.RuntimeType == typeof(string); }
        }
        public bool IsEnum {
            get { return ElementTypeOrSelf.RuntimeType.IsEnum; }
        }
        public bool CanBeOptional {
            get { return m_type.CanBeNull(); }
        }

        public object Parse(string value) {

            if (string.IsNullOrEmpty(value))
                value = null;

            // nullable
            if (IsNullable) {

                if (value == null && m_type != typeof(bool?))
                    return null;

                return ElementTypeOrSelf.Parse(value);
            }

            // enumerations
            if (IsEnumeration) {
                if (value == null)
                    return Array.CreateInstance(ElementTypeOrSelf.RuntimeType, 0);

                return value.StringSplit(';').Select(o => ElementTypeOrSelf.Parse(o)).ToList();
            }

            // string
            if (m_type == typeof(string))
                return value;

            // char
            if (m_type == typeof(char)) {
                if (value.Length > 1)
                    throw ParseException("cannot parse '" + value + "' as a char because its more than one character.");
                return value[0];
            }

            // bool
            if (m_type == typeof(bool)) {

                // switches without values are implicitly true
                if (value == null)
                    return true;

                if (value == "true")
                    return true;

                if (value == "false")
                    return false;

                throw ParseException("cannot parse '" + value +
                    "' as a bool because booleans must have a value of either 'true' or 'false'.");
            }

            // enum
            if (m_type.IsEnum)
                return Enum.Parse(m_type, value, true);

            // primitives
            Contract.Assert(m_type.IsPrimitive);
            {

                var numberStyle = default(NumberStyles);
                object result = null;

                // allow hex style numbers
                var hexRegex = Regex.Match(value, "^0x(?<value>[0-9a-fA-F]+)$", RegexOptions.IgnoreCase);
                if (hexRegex.Success) {
                    numberStyle = NumberStyles.HexNumber;
                    value = hexRegex.Get("value");
                }

                // constrain other number formats
                if ((!hexRegex.Success && !Regex.IsMatch(value, "^[-]?[0-9]+([.][0-9]+)?$")) ||
                    !Primitive.TryParse(m_type, value, numberStyle, null, out result))
                    throw ParseException("cannot parse '" + value +
                        "' as a number because it is not a recognized format.");

                return result;
            }
        }
        public string ToString(object value) {
            if (value == null)
                return null;

            return value.ToString();
        }

        public override string ToString() {
            return m_type.ToString();
        }
    }

    public abstract class Command {

        [ThreadStatic]
        public static int ErrorLevel;

        //public override string ToString() {
        //    var info = CommandInfo.GetCommand(GetType());

        //    return new TextJoin() {
        //        info.Assembly.Name,
 
        //        info.Name,

        //        from o in info.Parameters()
        //        let value = o.GetValue(this)
        //        where value != null
        //        let toString = o.ParameterType.ToString(value)
        //        let quote = toString.Contains(' ') ? "\"" : null
        //        select new TextNodes() { 
        //            o.IsTarget ? null : new TextNodes() {
        //                "/", o.Name, ":"
        //            }, 
        //            quote, o.ParameterType.ToString(value), quote
        //        },
        //    }.ReadToEnd();
        //}
    }

    public abstract class CommandAttributeAttribute : Attribute, ICompileTimeConstraint {
        protected CommandAttributeAttribute() { }

        public void OnPostCompile(TaskLoggingHelper log, ICustomAttributeProvider target) {
        }
    }

    [AttributeUsage(AttributeTargets.All, Inherited = false)]
    public sealed class CommandAttribute : CommandAttributeAttribute {

        public string Name { get; set; }
    }

    [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)]
    public class CommandMethodAttribute : CommandAttributeAttribute, IMethodThunkAspect {
        
        public object Thunk(int methodToken, object target, object[] argument) {
            throw new NotImplementedException();
        }
    }

    [AttributeUsage(AttributeTargets.Method, Inherited = false)]
    public sealed class CommandVerbAttribute : CommandMethodAttribute {
    }

    /// <summary>
    /// Supply additional metadata to parameters of CommandMethods which affect deserialization of 
    /// commmand line arguments and the auto documentation via help.
    /// </summary>
    public abstract class CommandParameterAttribute : CommandAttributeAttribute, IParameterAspect {

        /// <summary>
        /// Whether this parameter is a path, if it's a file or directory, if it exists, and if it's
        /// relative to the working directory.
        /// </summary>
        public CommandPathInfo Path { get; set; }

        public object OnArgument(int parameterToken, object argument) {
            throw new NotImplementedException();
        }
    }
    public sealed class CommandSwitchAttribute : CommandParameterAttribute {
    }
    public sealed class CommandTargetAttribute : CommandParameterAttribute {
    }

    public sealed class CommandArgument : TextNodes {

        protected override IEnumerable<object> Children(TextNodeContext context) {
            var containsWhiteSpace = false;

            var result = Children();
            context.Evaluate(result, 
                delegate(char character) { 
                    return !(containsWhiteSpace = char.IsWhiteSpace(character)); 
                }
            );

            if (containsWhiteSpace)
                return new TextNodes() { "\"", result, "\"" };
            
            return result;
        }
    }
}